#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <unordered_map>
#include <mutex>
#include "util.hpp"
#include "log.hpp"

namespace ns_index
{
//DocInfo:描述文档内容
struct DocInfo
{ 
    std::string title;   //文档的标题
    std::string content; //文档对应的去标签之后的内容
    std::string url;     //官网文档url
    uint64_t doc_id;     //文档的ID,用uint64_t:为了防止索引越界
};

struct InvertedElem
{    
    uint64_t doc_id;//对应的文档id
    std::string word;//关键字
    int weight; //权重  可以根据权重决定文档显示的先后顺序
    InvertedElem():weight(0){}
};

typedef std::vector<InvertedElem> InvertedList;// 倒排拉链
class Index 
{
private:
    std::vector<DocInfo> forward_index;                           // 正排拉链
    std::unordered_map<std::string, InvertedList> inverted_index; //存放关键字和倒排拉链的映射关系
public:
    Index(){} //但是一定要有函数体，不能delete
    Index(const Index&) = delete;
    Index& operator=(const Index&) = delete;
    static Index* instance;//指向全局唯一的单例对象
    static std::mutex mtx;
public:
    ~Index(){}
    //获取全局唯一的单例对象
    static Index* GetInstance()
    {
         //第一次需要加锁,后面都不需要加锁的场景,可以使用双检查加锁
         //特点：第一次加锁,后面不加锁,保护线程安全,同时提高效率
        if(nullptr == instance)
        {
            //加锁只有第一次有意义,后面再有线程来没必要加锁,加锁会引发效率低下
            mtx.lock();
            if(nullptr == instance)
            {
                instance = new Index();
            }
            mtx.unlock();
        }
        return instance;//返回这个全局的单例对象
    }
    //根据文档id doc_id找到找到文档内容   正排索引:通过id找到文档内容(正排索引节点)
    DocInfo* GetForwardIndex(uint64_t doc_id) 
    {    
        if(doc_id >= forward_index.size()) //没有该文档id
        {
            std::cerr << "doc_id out range, error!" << std::endl;
            return nullptr;
        }
        return &forward_index[doc_id]; //数组的下标天然就是文档id,所以直接返回数组对应的内容的地址
    }

    //根据关键字word，获得倒排拉链 
    InvertedList *GetInvertedList(const std::string &word)
    {
        auto iter = inverted_index.find(word);//在哈希表当中查找是否存在这个关键字对应的倒排拉链
        if(iter == inverted_index.end())
        {
            std::cerr << word << " have no InvertedList" << std::endl;
            return nullptr;
        }
        return &(iter->second); //InvertedList
    }


    //参数:根据文档内容构造索引,参数:经过数据清洗之后的文件,input:其实就是data/raw_html/raw.txt
    bool BuildIndex(const std::string &input) //根据去标签,格式化之后的文档,构建正排和倒排索引
    {
        std::ifstream in(input, std::ios::in | std::ios::binary);
        if(!in.is_open()){ 
            std::cerr << "build index error, open " << input << " failed" << std::endl;
            return false;
        }

        int count = 0;//方便观察当前建立的索引个数
        std::string line; //读取一行数据 ->一个html文件去标签,格式化之后的文档内容
        while(std::getline(in, line)) //对每一个html文件格式化之后的内容进行正排和倒排索引
        {
            DocInfo * doc = BuildForwardIndex(line); //建立正排索引,返回描述这个文档内容的节点
            if(nullptr == doc)
            { 
                std::cerr << "build " << line << " error" << std::endl; //for deubg
                continue;
            }
            BuildInvertedIndex(*doc);//根据上面返回的正排索引节点,建立倒排索引

            count ++;
            if(count % 150 == 0)
            {
            //std::cout <<"当前已经建立的索引文档: " << count <<std::endl;
            LOG(NORMAL,"当前的已经建立的索引文档:"+std::to_string(count));
            }
        }
        return true;
    }
private:
    //line:就是一个html文件里面去标签,格式化之后的文档内容
    DocInfo *BuildForwardIndex(const std::string &line) //建立正排索引
    {
        //1. 解析line->本质是进行字符串切分,解析成:title, content, url  
        std::vector<std::string> results;//保存的是切分好内容
        const std::string sep = "\3";   //分隔符
        ns_util::StringUtil::Split(line, &results, sep); //切分字符串->单独封装在了util.hpp里面
        if(results.size() != 3)		//判断切分结果是否正确,要切分为3部分
        { 
            return nullptr;
        }
        //2. 切分好的字符串进行填充到DocIinfo(正排索引节点)
        DocInfo doc;
        doc.title = results[0]; //title
        doc.content = results[1]; //content
        doc.url = results[2];   ///url
        //此时的先进行保存id,然后再插入,此时对应的id就是当前doc在vector中的下标
        doc.doc_id = forward_index.size(); 
        //3. 插入到正排索引数组当中
        forward_index.push_back(std::move(doc)); 
        return &forward_index.back(); //返回这个新插入的内容,描述这个文档内容的正排索引节点
    }

    bool BuildInvertedIndex(const DocInfo &doc)//建立倒排索引,参数是正排索引节点
    {
        //DocInfo里面包含一个html文件的:{title, content, url, doc_id}
        struct word_cnt //针对一个词的数据统计
        {   
            int title_cnt;//在标题出现次数
            int content_cnt;//在内容中出现次数
            word_cnt()
            :title_cnt(0)
            , content_cnt(0){}
        };
        std::unordered_map<std::string, word_cnt> word_map; //暂存词频的映射表
        /* 根据文档标题和内容,分词并进行词频统计 */

        //对标题进行分词
        std::vector<std::string> title_words;
        ns_util::JiebaUtil::CutString(doc.title, &title_words);
        //对标题分词之后的结果进行词频统计
        for(std::string s : title_words)
        {  
            boost::to_lower(s); //字符串全部转为小写
            //查找对应关键词,如果存在就++，不存在就新建  
            word_map[s].title_cnt++; 
        }

        //对内容进行分词
        std::vector<std::string> content_words;
        ns_util::JiebaUtil::CutString(doc.content, &content_words);//对文档内容分词
        //对内容进行词频统计
        for(std::string s : content_words) 
        {
            boost::to_lower(s);
            word_map[s].content_cnt++;
        }

        /* 建立分词之后的词和倒排拉链的映射 */
        #define X 15  //标题当中出现的词,权重更高
        #define Y 1
        for(auto &word_pair : word_map)
        {
            InvertedElem item;//构建倒排索引结构节点
            item.doc_id = doc.doc_id;
            item.word = word_pair.first;//unordered_map<std::string, word_cnt> word_map
            item.weight = X*word_pair.second.title_cnt +Y*word_pair.second.content_cnt;  //设置权重
            InvertedList& inverted_list = inverted_index[word_pair.first];
            inverted_list.push_back(std::move(item));
        }
        return true;
    }
};
//静态成员在类外初始化:
Index* Index::instance = nullptr;
std::mutex Index::mtx; //因为有构造函数,自动就会初始化
}